// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <QtCore/QList>
#include <QtCore/QMutex>

#include "config/configverifier.h"
#include "util/provider.h"

// include platform-appropriate verifiers here:
#ifdef _WIN32
#include "config/outlook.h"
#include "config/registrykey.h"
#endif  // _WIN32
#ifdef __APPLE__
#include "config/applemail.h"
#endif  // __APPLE__
#include "config/thunderbird.h"


namespace popdiag {

namespace {
class Registry {
 public:
  Registry() {
  }

  ~Registry() {
    for (QList<ConfigVerifier *>::const_iterator i = verifiers_.begin();
         i != verifiers_.end(); ++i) {
      delete *i;
    }
    verifiers_.clear();
  }

  void Register(ConfigVerifier *c) {
    // Must be called under lock.
    verifiers_.append(c);
  }

  void GetVerifiers(QList<ConfigVerifier *> *out) {
    QMutexLocker lock(&lock_);
    Initialize();
    out->clear();
    for (QList<ConfigVerifier *>::const_iterator i = verifiers_.begin();
         i != verifiers_.end(); ++i) {
      out->append(*i);
    }
  }

 private:
  void Initialize() {
    // Must be called under lock.
    // We can't do this at static construction time, as we have to wait for
    // the translation catalogs to be loaded.
    if (verifiers_.isEmpty()) {
      // register platform-appropriate verifiers here:
#ifdef _WIN32
      Register(new Outlook(OUTLOOK_2007));
      Register(new Outlook(OUTLOOK_2003));
      Register(new Outlook(OUTLOOK_2002));
      Register(new Outlook(OUTLOOK_2000));
      Register(new Outlook(OUTLOOK_EXPRESS_6));
      Register(new Outlook(WINDOWS_MAIL));
      Register(new Outlook(WINDOWS_LIVE_MAIL));
#endif  // _WIN32
#ifdef __APPLE__
      Register(new AppleMail());
#endif  // __APPLE__
      Register(new Thunderbird());
    }
  }

  QMutex lock_;
  QList<ConfigVerifier *> verifiers_;
};

static Registry registry;

}  // anonymous namespace

class FaultDescriber : public QObject {
 public:
  static QString Describe(const Fault &f) {
    switch (f.first) {
      case PROFILE_NOT_UNDERSTOOD:
        return tr("We could not read this profile.",
                  "This program could not understand this "
                  "client configuration.");
      case POP3_SERVER:
        return tr("Your POP server name is incorrect. "
                  "It should be %1",
                  "Requests that the user correct the pop server name. "
                  "%1 is the name of the POP server")
               .arg(provider->PopServer());
      case POP3_PORT:
        return tr("Your POP server port is incorrect. "
                  "It should be %1",
                  "Requests that the user correct the pop server port. "
                  "%1 is the POP port number")
               .arg(POP_SSL_PORT);
      case POP3_USE_SSL:
        return tr("You must configure your client to connect to "
                  "the POP server using SSL",
                  "Requests that the user set POP to use the "
                  "Secure Sockets Layer (SSL)");
      case POP3_LEAVE_ON_SERVER:
        return tr("Your client is configured to leave messages on "
                  "the server. "
                  "This will not work properly with %1 POP.",
                  "Requests that the user change the POP server "
                  "'leave on server' settings. "
                  "%1 is the company name")
                 .arg(provider->Name());
      case POP3_USE_SPA:
        return tr("Do not use Microsoft SPA to authenticate "
                  "to %1 POP. Please turn this option off.",
                  "Asks the user to change the POP authentication "
                  "settings to not use the Microsoft-proprietary "
                  "Secure Password Authentication (SPA).  You may "
                  "Need to run Outlook Express (msimn.exe) and look at "
                  "The POP server settings in the target language to "
                  "learn what this is called in that language. "
                  "%1 is the company name")
                .arg(provider->Name());
      case POP3_USERNAME:
        return tr("Your username for %1 POP is incorrect. "
                  "It should be your %1 email address",
                  "Request that the user change the username for POP "
                  "to his email address. %1 is the company name")
               .arg(provider->Name());
      case POP3_TIMEOUT:
        return tr("Your POP timeout is too short. "
                  "It should be at least one minute.",
                  "Requests that the user use a long server timeout.");
      case POP3_ACCOUNT_SKIPPED:
        return tr("This account is skipped when fetching new mail.",
                  "Notifies the user that mail for this account is not "
                  "retrieved on a regular basis.");
      case SMTP_SERVER:
        return tr("Your SMTP server should be %1."
                  "Requests that the user correct the smtp server name")
                 .arg(provider->SmtpServer());
      case SMTP_PORT_465:
        return tr("Your SMTP server port should be %1.",
                  "Requests that the user correct the smtp server port. "
                  "%1 is the MSA SSL port number")
                 .arg(SMTP_MSA_SSL_PORT);
      case SMTP_PORT_587:
        return tr("Your SMTP server port should be %1.",
                  "Requests that the user correct the smtp server port. "
                  "%1 is the MSA TLS port number")
                 .arg(SMTP_MSA_TLS_PORT);
      case SMTP_USE_AUTH:
        return tr("You must configure your mail client to "
                  "authenticate when using %1 SMTP.",
                  "Requests that the user change the SMTP server "
                  "settings to use authentication. "
                  "%1 is the company name")
                 .arg(provider->Name());
      case SMTP_USE_SSL:
        return tr("You must configure your mail client to use "
                  "SSL or TLS when using %1 SMTP.",
                  "Requests that the user set POP to use the "
                  "Secure Sockets Layer (SSL) or Transport Layer "
                  "Security (TLS). %1 is the company name")
                 .arg(provider->Name());
      case SMTP_USERNAME:
        return tr("Your username for %1 SMTP is incorrect. "
                  "It should be your %1 email address",
                  "Request that the user change the username for POP. "
                  "%1 is the company name")
                 .arg(provider->Name());
      case SMTP_USE_SPA:
        return tr("Do not use Microsoft SPA to authenticate to "
                  "%1 SMTP. Please turn this option off.",
                  "Asks the user to change the SMTP authentication "
                  "settings to not use the Microsoft-proprietary "
                  "Secure Password Authentication (SPA).  You may "
                  "Need to run Outlook Express (msimn.exe) and look at "
                  "The SMTP server settings in the target language to "
                  "learn what this is called in that language. "
                  "%1 is the company name")
                 .arg(provider->Name());
      case SMTP_SPLIT_MESSAGES:
        return tr("You mail client is configured to split outgoing "
                  "mail messages into small parts. "
                  "This is not necessary.",
                  "Asks the user to change the client configuration so "
                  "that it does not fragment long messages.");
      case SMTP_TIMEOUT:
        return tr("Your SMTP timeout is too short. "
                  "It should be at least one minute.",
                  "Requests that the user use a long server timeout.");
      case SMTP_EMAIL_ADDRESS:
        // TODO: This will mis-detect custom-from addresses as wrong.
        return tr("The email address configured for sending "
                  "via SMTP is incorrect.",
                  "Requests that the user set their 'from' address for "
                  "sending mail to be their email address.");
      case SMTP_AUTH_POP_BEFORE_SMTP:
        return tr("Your SMTP settings are configured for "
                  "POP-before-SMTP."
                  " Please setup SMTP to login to SMTP directly.",
                  "Some mail clients offer an SMTP authentication mode "
                  "whereby they login to the POP server before sending "
                  "mail. This provider doesn't support that.  This requests "
                  "that the user change to using straight authenticated "
                  "SMTP.");
      case IMAP_SERVER:
        return tr("Your IMAP server name is incorrect. "
                  "It should be %1",
                  "Requests that the user correct the imap server name. "
                  "%1 is the IMAP server")
                 .arg(provider->ImapServer());
      case IMAP_PORT:
        return tr("Your IMAP server port is incorrect. "
                  "It should be %1",
                  "Requests that the user correct the imap server port. "
                  "%1 is the IMAP port")
                 .arg(IMAP_SSL_PORT);
      case IMAP_USE_SSL:
        return tr("You must configure your client to connect to "
                  "the IMAP server using SSL",
                  "Requests that the user set IMAP to use the "
                  "Secure Sockets Layer (SSL)");
      case IMAP_USE_SPA:
        return tr("Do not use Microsoft SPA to authenticate "
                  "to %1 IMAP. Please turn this option off.",
                  "Asks the user to change the IMAP authentication "
                  "settings to not use the Microsoft-proprietary "
                  "Secure Password Authentication (SPA).  You may "
                  "Need to run Outlook Express (msimn.exe) and look at "
                  "the IMAP server settings in the target language to "
                  "learn what this is called in that language. "
                  "%1 is the company name")
                 .arg(provider->Name());
      case IMAP_USERNAME:
        return tr("Your username for %1 IMAP is incorrect. "
                  "It should be your %1 email address",
                  "Request that the user change the username for IMAP. "
                  "%1 is the company name")
                 .arg(provider->Name());
      case IMAP_FOLDER_PATH:
        return tr("Your Folder Path is incorrect. It should be blank",
                  "Request that the user leave the Folder Path blank "
                  "on the Advanced settings panel.");
      case UNKNOWN:
        // fall through to default message
        ;
    }
    return tr("We encountered an unknown problem with our program.",
              "This should NOT happen. An unknown fault code was "
              "encountered.");
  }
};

VerificationResult::VerificationResult()
    : display_protocol_("POP"),
      is_default_program_(false),
      is_default_profile_(false),
      imap_server_(provider->ImapServer()),
      pop_server_(provider->PopServer()),
      smtp_server_(provider->SmtpServer()),
      smtp_port_(SMTP_MSA_SSL_PORT) {
}

VerificationResult::~VerificationResult() {
}

const QString &VerificationResult::program_name() const {
  return program_name_;
}

bool VerificationResult::is_default_program() const {
  return is_default_program_;
}

bool VerificationResult::is_default_profile() const {
  return is_default_profile_;
}

const QString &VerificationResult::program_code() const {
  return program_code_;
}

const QString &VerificationResult::profile_name() const {
  return profile_name_;
}

const QString &VerificationResult::display_protocol() const {
  return display_protocol_;
}

const QString &VerificationResult::display_email() const {
  return display_email_;
}

const QString &VerificationResult::display_name() const {
  return display_name_;
}

const FaultSet &VerificationResult::faults() const {
  return faults_;
}

const QString &VerificationResult::debug_text() const {
  return debug_text_;
}

void VerificationResult::set_program_name(const QString &n) {
  program_name_ = n;
}

void VerificationResult::set_program_code(const QString &n) {
  program_code_ = n;
}

void VerificationResult::set_profile_name(const QString &n) {
  profile_name_ = n;
}

void VerificationResult::set_display_protocol(const QString &n) {
  display_protocol_ = n;
}

void VerificationResult::set_display_email(const QString &n) {
  display_email_ = n;
}

void VerificationResult::set_display_name(const QString &n) {
  display_name_ = n;
}

void VerificationResult::set_is_default_program(bool n) {
  is_default_program_ = n;
}

void VerificationResult::set_is_default_profile(bool n) {
  is_default_profile_ = n;
}

void VerificationResult::set_faults(const FaultSet &s) {
  faults_ = s;
}

void VerificationResult::set_debug_text(const QString &n) {
  debug_text_ = n;
}

void VerificationResult::ClearLog() {
  debug_text_.clear();
}

void VerificationResult::StartLogDetails() {
  debug_text_ += "<table border=1 class=\"config\">\n";
  debug_text_ += "<tr><th>Step</th><th>OK?</th><th>Details</th></tr>\n";
}

void VerificationResult::LogDetails(const QString &step, bool good,
                                    const QString &details) {
  const char *color = good ? "#00FF00" : "#ff0000";
  debug_text_ += QString("<tr BGCOLOR=\"%1\">").arg(color);
  debug_text_ += QString("<td>%1</td><td>%2</td><td>%3</td></tr>\n")
                 .arg(step).arg(good ? "OK" : "BAD")
                 .arg(details.isEmpty() ? "&nbsp;" : details);
}

void VerificationResult::EndLogDetails() {
  debug_text_ += "</table>\n";
}

void VerificationResult::InitProtocolDependentValues() {
  switch (server_type_) {
    case SERVERTYPE_POP:
      protocol_name_ = "POP";
      username_fault_ = POP3_USERNAME;
      protocol_ = &Protocol::POP();
      server_fault_ = POP3_SERVER;
      use_ssl_fault_ = POP3_USE_SSL;
      ssl_port_ = POP_SSL_PORT;
      plaintext_port_ = POP_PLAINTEXT_PORT;
      port_fault_ = POP3_PORT;
      incoming_server_ = &pop_server_;
      break;
    case SERVERTYPE_IMAP:
      protocol_name_ = "IMAP";
      username_fault_ = IMAP_USERNAME;
      protocol_ = &Protocol::IMAP();
      server_fault_ = IMAP_SERVER;
      use_ssl_fault_ = IMAP_USE_SSL;
      ssl_port_ = IMAP_SSL_PORT;
      plaintext_port_ = IMAP_PLAINTEXT_PORT;
      port_fault_ = IMAP_PORT;
      incoming_server_ = &imap_server_;
      break;
  }
  set_display_protocol(protocol_name_);
}

void VerificationResult::CheckIncomingSettings() {
  if (protocol_->VerifyUsernameFromEmailAddress(incoming_user_name_, email_)) {
    LogDetails(protocol_name_ + " Username", true, incoming_user_name_);
  } else {
    LogDetails(protocol_name_ + " Username", false, incoming_user_name_);
    faults_.append(Fault(username_fault_, email_));
  }

  if (!protocol_->IsOurMailServer(incoming_server_name_)) {
    faults_.append(Fault(server_fault_, incoming_server_name_));
    LogDetails(protocol_name_ + " Server", false, incoming_server_name_);
  } else {
    LogDetails(protocol_name_ + " Server", true, incoming_server_name_);
  }
  *incoming_server_ = incoming_server_name_;

  LogDetails(protocol_name_ + " Use SSL", incoming_socket_type_ == SSL, "");
  if (incoming_socket_type_ != SSL) {
    faults_.append(Fault(use_ssl_fault_, ""));
  }
  LogDetails(protocol_name_ + " Port", incoming_port_ == ssl_port_,
             QString("%1").arg(incoming_port_));
  if (incoming_port_ != ssl_port_) {
    faults_.append(Fault(port_fault_, QString::number(incoming_port_)));
  }

  if (server_type_ == SERVERTYPE_POP) {
    LogDetails("POP Leave on Server", leave_on_server_ != true, "");
    if (leave_on_server_ == true && provider->PopMustNotLeaveOnServer()) {
      faults_.append(Fault(POP3_LEAVE_ON_SERVER, ""));
    }
  }
}

void VerificationResult::CheckOutgoingSettings() {
  if (provider->SmtpVerifyUsernameFromEmailAddress(smtp_username_, email_)) {
    LogDetails("SMTP Username", true, smtp_username_);
  } else {
    LogDetails("SMTP Username", false, smtp_username_);
    faults_.append(Fault(SMTP_USERNAME, smtp_username_));
  }
  if (smtp_email_ != email_) {
    LogDetails("SMTP Email Address", false, smtp_email_);
    faults_.append(Fault(SMTP_EMAIL_ADDRESS, smtp_email_));
  } else {
    LogDetails("SMTP Email Address", true, smtp_email_);
  }
  LogDetails("SMTP Use Auth", smtp_use_auth_ == 1, "");
  if (smtp_use_auth_ != 1) {
    faults_.append(Fault(SMTP_USE_AUTH, ""));
  }

  switch (smtp_socket_type_) {
    case SSL:
      LogDetails("SMTP Port", smtp_port_ == SMTP_MSA_SSL_PORT ||
                              provider->SmtpCanUsePlaintextPort(),
                 QString("%1").arg(smtp_port_));
      if (smtp_port_ != SMTP_MSA_SSL_PORT &&
          !provider->SmtpCanUsePlaintextPort()) {
        faults_.append(Fault(SMTP_PORT_465, QString::number(smtp_port_)));
      }
      break;
    case TLS_IF_AVAILABLE:  // fall through
    case TLS:
      LogDetails("SMTP Port", smtp_port_ == SMTP_MSA_TLS_PORT ||
                              provider->SmtpCanUsePlaintextPort(),
                 QString("%1").arg(smtp_port_));
      if (smtp_port_ != SMTP_MSA_TLS_PORT &&
          !provider->SmtpCanUsePlaintextPort()) {
        faults_.append(Fault(SMTP_PORT_587, QString::number(smtp_port_)));
      }
      break;
    case PLAINTEXT:  // fall through
    default:
      LogDetails("SMTP Port", false,
                 QString("%1").arg(smtp_port_));
      if (smtp_port_ != SMTP_MSA_TLS_PORT) {
        faults_.append(Fault(SMTP_PORT_587, QString::number(smtp_port_)));
      }
      faults_.append(Fault(SMTP_USE_SSL, ""));
      break;
  }
}

ConfigVerifier::ConfigVerifier() {
}

ConfigVerifier::~ConfigVerifier() {
}

void ConfigVerifierRegistry::GetVerifiers(QList<ConfigVerifier*> *verifiers) {
  registry.GetVerifiers(verifiers);
}

#if defined (_WIN32)
QString ConfigVerifierRegistry::GetDefaultMailProgramBaseName() {
  // Default mail program is set at HKCR\mailto\shell\open\command\(Default)
  const RegistryKey &rk = RegistryKey::ClassesRoot();
  scoped_ptr<RegistryKey> mailto(rk.ChildKey("mailto\\shell\\open\\command"));
  if (mailto == NULL) {
    // no default, I guess
    return "";
  }
  QString mailtocmd;
  mailto->ReadStringEntry("", &mailtocmd);

  // need a quote-aware split.  Tokenize on space unless quoted.
  // should be a quote-aware split ideally.  But none of the programs we can
  // read configurations for put spaces in the path for very wise reasons.

  // if the first character is a quote, take up to the second quote.
  // otherwise take up to the first space.
  QChar tokenchar = ' ';
  if (mailtocmd.length() > 2 && mailtocmd[0] == '"') {
    tokenchar = '"';
  }
  QString  cmd = mailtocmd.split(tokenchar, QString::SkipEmptyParts)[0];

  // now pull off the last path component
  QStringList parts = cmd.split('\\');
  QString basename = parts[parts.size() - 1].toLower();
  return basename;
}

#else   // __APPLE__, __unix

QString ConfigVerifierRegistry::GetDefaultMailProgramBaseName() {
  // TODO: Find out how to determine default mail program on OS X
  return "unknown";
}
#endif  // __APPLE__, __unix

ConfigChecker::ConfigChecker(QObject *parent) : QObject(parent) {
}

ConfigChecker::~ConfigChecker() {
}

void ConfigChecker::GatherConfigDetails(VerificationSet *faults_found) {
  QList<ConfigVerifier *> verifiers;
  ConfigVerifierRegistry::GetVerifiers(&verifiers);
  for (QList<ConfigVerifier *>::const_iterator i = verifiers.begin();
       i != verifiers.end(); ++i) {
    (*i)->CheckConfiguration(faults_found);
  }
  emit OnConfigsChecked(*faults_found);
}

QString FaultDescription(const Fault &f) {
  return FaultDescriber::Describe(f);
}

}  // namespace popdiag
